{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<center>\n",
    "<img src=\"../../img/ods_stickers.jpg\">\n",
    "</center>\n",
    "    \n",
    "## Открытый курс по машинному обучению\n",
    "Автор материала: программист-исследователь Mail.ru Group, старший преподаватель Факультета Компьютерных Наук ВШЭ Юрий Кашницкий. Материал распространяется на условиях лицензии [Creative Commons CC BY-NC-SA 4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/). Можно использовать в любых целях (редактировать, поправлять и брать за основу), кроме коммерческих, но с обязательным упоминанием автора материала."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# <center> Соревнование по прогнозированию популярности статьи на портале Medium\n",
    "## <center> Ridge baseline"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "[Ссылка](https://mlcourse.arktur.io/) на соревнование. \n",
    "\n",
    "**Задача** \n",
    "\n",
    "Есть выборка статей с популярного англоязычного портала Medium. Задача – спрогнозировать число рекомендаций (\"лайков\") статьи.\n",
    "Предлагается Вам самим составить обучающую и тестовую выборки на основе имеющихся данных, обучить модель-регрессор и сформировать файл посылки с прогнозами – числом рекомендаций статей (с `log1p`-преобразованием) из тестовой выборки.\n",
    "\n",
    "**Данные**\n",
    "\n",
    "Обучающая выборка – 52699 статей, опубликованных до 2016 года включительно (**train.zip** ~ 480 Mb, unzip ~1.6 Gb). Тестовая выборка – 39492 статьи, опубликованные с 1 января по 27 июня 2017 года (**test.zip** ~425 Mb, unzip ~1.4 Gb).\n",
    "\n",
    "Данные о статьях представлены в JSON формате с полями:\n",
    "- _id и url – URL статьи\n",
    "- published – время публикации\n",
    "- title – название статьи\n",
    "- author – имя автора, его акканут на Твиттере и Medium\n",
    "- content – HTML-контент статьи\n",
    "- meta_tags – остальная информация о статье\n",
    "\n",
    "В файле **train_log1p_recommends.csv** представлены номера (id) статей из обучающей выборки вместе с целевым показателем: числом рекомендаций статей, к которому применено преобразование `log1p(x) = log(1 + x)` В файле **sample_submission.csv** представлен пример файла посылки."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!ll ../../data/t"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import json\n",
    "import pickle\n",
    "from tqdm import tqdm_notebook\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from glob import glob\n",
    "from matplotlib import pyplot as plt\n",
    "\n",
    "%matplotlib inline\n",
    "from sklearn.feature_extraction.text import CountVectorizer\n",
    "from sklearn.metrics import mean_absolute_error\n",
    "from sklearn.linear_model import SGDRegressor"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "path_to_train = \"../../data/train.json\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "with open(path_to_train) as inp_json:\n",
    "    first_line = inp_json.readline()\n",
    "    article_data_json = json.loads(first_line)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "article_data_json.keys()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "article_data_json[\"quality\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "article_data_json[\"url\"]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "article_data_json[\"author\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Следующий код я стащил откуда-то со StackOverflow – он выкидывает из текста HTML-теги."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from html.parser import HTMLParser\n",
    "\n",
    "\n",
    "class MLStripper(HTMLParser):\n",
    "    def __init__(self):\n",
    "        self.reset()\n",
    "        self.strict = False\n",
    "        self.convert_charrefs = True\n",
    "        self.fed = []\n",
    "\n",
    "    def handle_data(self, d):\n",
    "        self.fed.append(d)\n",
    "\n",
    "    def get_data(self):\n",
    "        return \"\".join(self.fed)\n",
    "\n",
    "\n",
    "def strip_tags(html):\n",
    "    s = MLStripper()\n",
    "    s.feed(html)\n",
    "    return s.get_data()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Готовим обучающую и тестовую выборки. Забираем из JSON-представления статьи только content (собственно текст статьи), очищаем его от HTML-тегов и записываем в файл. Такой формат подойдет для извлечения признаков (Bag of Words) с помощью `CountVectorizer`. На Mac с SSD это все работает относительно быстро, на Windows без SSD будет скучновато."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "with open(os.path.join(PATH_TO_DATA, \"train.json\")) as inp_json_file, open(\n",
    "    os.path.join(PATH_TO_DATA, \"train_raw_content.txt\"), \"w\"\n",
    ") as out_raw_text_file:\n",
    "\n",
    "    for line in tqdm_notebook(inp_json_file):\n",
    "        json_data = json.loads(line)\n",
    "        content = json_data[\"content\"].replace(\"\\n\", \" \")\n",
    "        content_no_html_tags = strip_tags(content)\n",
    "        out_raw_text_file.write(content_no_html_tags + \"\\n\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!wc -l $PATH_TO_DATA/train_raw_content.txt"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "with open(os.path.join(PATH_TO_DATA, \"test.json\")) as inp_json_file, open(\n",
    "    os.path.join(PATH_TO_DATA, \"test_raw_content.txt\"), \"w\"\n",
    ") as out_raw_text_file:\n",
    "\n",
    "    for line in tqdm_notebook(inp_json_file):\n",
    "        json_data = json.loads(line)\n",
    "        content = json_data[\"content\"].replace(\"\\n\", \" \")\n",
    "        content_no_html_tags = strip_tags(content)\n",
    "        out_raw_text_file.write(content_no_html_tags + \"\\n\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!wc -l $PATH_TO_DATA/test_raw_content.txt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Долго возился с этим багом – `CountVectorizer` возвращал больше строк, чем надо, из-за ^M (возврата каретки) – следующий код, тоже позаимствованный откуда до со StackOverflow, убирает эти символы. Черт побери... перл. Но работает быстро :)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!perl -p -i -e \"s/\r",
    "//g\" $PATH_TO_DATAtrain_raw_content.txt\n",
    "!perl -p -i -e \"s/\r",
    "//g\" $PATH_TO_DATAtest_raw_content.txt"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Теперь как раз применяем `CountVectorizer`. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cv = CountVectorizer(max_features=100000)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "with open(os.path.join(PATH_TO_DATA, \"train_raw_content.txt\")) as input_train_file:\n",
    "    X_train = cv.fit_transform(input_train_file)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_train.shape"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "with open(os.path.join(PATH_TO_DATA, \"test_raw_content.txt\")) as input_test_file:\n",
    "    X_test = cv.transform(input_test_file)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X_test.shape"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Считываем ответы на обучающей выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_target = pd.read_csv(\n",
    "    os.path.join(PATH_TO_DATA, \"train_log1p_recommends.csv\"), index_col=\"id\"\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y_train = train_target[\"log_recommends\"].values"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Будем смотреть на качество (MAE) на 30% данных, причем не перемешиваем данные, а соблюдаем время – проверочная часть четко по времени после обучающей."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_part_size = int(0.7 * train_target.shape[0])\n",
    "X_train_part = X_train[:train_part_size, :]\n",
    "y_train_part = y_train[:train_part_size]\n",
    "X_valid = X_train[train_part_size:, :]\n",
    "y_valid = y_train[train_part_size:]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Посмотрим на распределение логарифма целевого признака. То есть это уже 2 логарифма от числа рекомендаций статьи."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.hist(np.log(y_train_part), bins=15);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Обучаем Ridge-регрессию без настройки гиперпараметров на 70% исходной обучающей выборки."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.linear_model import Ridge"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ridge_reg = Ridge(random_state=17)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "ridge_reg.fit(X_train_part, y_train_part)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ridge_valid_pred = ridge_reg.predict(X_valid)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mean_absolute_error(y_valid, ridge_valid_pred)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Получили MAE $\\approx$ 1.25. Если вренуться к числу рекомендаций статьи, то ошибаемся в среднем на 2.5 единицы. Теперь обучаем такую же модель, но на всей обчающей выборке."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "ridge_reg.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Делаем прогноз для тестовой выборки."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%time\n",
    "ridge_test_pred = ridge_reg.predict(X_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Записываем прогнозы в файл. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def write_submission_file(\n",
    "    prediction,\n",
    "    filename,\n",
    "    path_to_sample=os.path.join(PATH_TO_DATA, \"sample_submission.csv\"),\n",
    "):\n",
    "    submission = pd.read_csv(path_to_sample, index_col=\"id\")\n",
    "\n",
    "    submission[\"log_recommends\"] = prediction\n",
    "    submission.to_csv(filename)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "write_submission_file(ridge_test_pred, os.path.join(PATH_TO_DATA, \"first_ridge.csv\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Если сделать посылку на [сайте](https://mlcourse.arktur.io/dashboard?problem=MLCourse) соревнования, то получится воспроизведение бенчмарка \"Content only, Ridge + CountVectorizer\"."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
